#include "BankDatabase.h"
#include "macros.h"
#include <sstream>
#include <stdexcept>
#include <gtest/gtest.h>

using namespace std;

//////////////////////
//                  //
// Class definition //
//                  //
//////////////////////

BankDatabase::BankDatabase () : m_dbIsOpened (false), m_dbName ("")
  {}

BankDatabase::BankDatabase (string dbName) : m_dbIsOpened (false), m_dbName ("")
  {
  Open (dbName);
  }

BankDatabase::~BankDatabase ()
  {
  Close ();
  }

void
BankDatabase::Open (std::string dbName)
  {
  m_dbName = dbName;

  // open a connection to the database file
  int status = sqlite3_open (dbName.c_str (), &m_db);
  SQLITE_STATUS_CHECK (status, "Unable to open the database \"" << dbName << "\" - ");
  
  // create the table if it does not exist yet
  char* errmsg;
  status = sqlite3_exec (m_db,
    "create table if not exists BankTable (\
    CustomerId text primary key on conflict replace,\
    Balance real default 0);",
    NULL, 
    NULL,
    &errmsg
    );
  SQLITE_STATUS_CHECK (status, errmsg << " - ");
  sqlite3_free (errmsg);

  // create the UpdateCount table if it does not exist yet
  status = sqlite3_exec (m_db,
    "create table if not exists UpdateCounter (\
    ClientId text primary key on conflict replace,\
    UpdateCount integer default 0);",
    NULL, 
    NULL,
    &errmsg
    );
  SQLITE_STATUS_CHECK (status, errmsg << " - ");
  sqlite3_free (errmsg);

  // create sqlite3 prepared statement for setting balance
  status = sqlite3_prepare_v2 (m_db,
    "insert or replace into BankTable (\
    CustomerId, \
    Balance) values (\
    @cusId,\
    @balance);",
    -1,
    &m_setBalanceStmt,
    NULL);
  SQLITE_STATUS_CHECK (status, "Unable to prepare an INSERT statement for BankTable - ");

  // create sqlite3 prepared statment for getting balance
  status = sqlite3_prepare_v2 (m_db,
    "select Balance from BankTable where CustomerId = @cusId;",
    -1,
    &m_getBalanceStmt,
    NULL
    );
  SQLITE_STATUS_CHECK (status, "Unable to prepare a SELECT statement for BankTable - ");

  // create sqlite3 prepared statment for touching a customer
  status = sqlite3_prepare_v2 (m_db,
    "insert or ignore into BankTable (CustomerId) values (@cusId);",
    -1,
    &m_touchCustomerStmt,
    NULL
    );
  SQLITE_STATUS_CHECK (status, "Unable to prepare a INSERT or IGNORE statement for BankTable table - ");

  // create sqlite3 prepared statement for setting UpdateCount
  status = sqlite3_prepare_v2 (m_db,
    "insert or replace into UpdateCounter (\
    ClientId, \
    UpdateCount) values (\
    @clientId,\
    @updateCount);",
    -1,
    &m_setUpdateCountStmt,
    NULL);
  SQLITE_STATUS_CHECK (status, "Unable to prepare an INSERT statement for UpdateCounter table - ");

  // create sqlite3 prepared statment for reading UpdateCount
  status = sqlite3_prepare_v2 (m_db,
    "select UpdateCount from UpdateCounter where ClientId = @clientId;",
    -1,
    &m_getUpdateCountStmt,
    NULL
    );
  SQLITE_STATUS_CHECK (status, "Unable to prepare a SELECT statement for UpdateCounter table - ");

  m_dbIsOpened = true;
  }

void 
BankDatabase::Close ()
  {
  if (m_dbIsOpened)
    {
    int status = sqlite3_finalize (m_setBalanceStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_setBalanceStmt\" - ");
    status = sqlite3_finalize (m_getBalanceStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_getBalanceStmt\" - ");
    status = sqlite3_finalize (m_getUpdateCountStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_getBalanceStmt\" - ");
    status = sqlite3_finalize (m_setUpdateCountStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_getBalanceStmt\" - ");
    status = sqlite3_finalize (m_touchCustomerStmt);
    SQLITE_STATUS_CHECK (status, "Unable to finalize \"m_touchCustomerStmt\" - ");

    status = sqlite3_close (m_db);
    SQLITE_STATUS_CHECK (status, "Unable to close the database connection - ");

    m_dbIsOpened = false;
    }
  }

string 
BankDatabase::GetDatabaseName () const
  {
  return m_dbName;
  }

void 
BankDatabase::TouchCustomer (std::string cusId)
  {
  // bind the parameters into the prepared statement
  int status = sqlite3_bind_text (m_touchCustomerStmt, 
    sqlite3_bind_parameter_index (m_touchCustomerStmt, "@cusId"),
    cusId.c_str (),
    cusId.size (),
    SQLITE_STATIC
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind cusId (" << cusId << ") into m_touchCustomerStmt - ");

  // execute the SQL statement
  status = sqlite3_step (m_touchCustomerStmt);
  SQLITE_STATUS_CHECK (status, "Unable to execute \"m_touchCustomerStmt\" - ");
  status = sqlite3_reset (m_touchCustomerStmt);
  SQLITE_STATUS_CHECK (status, "Unable to reset \"m_touchCustomerStmt\" - ");
  }

void 
BankDatabase::SetBalance (std::string cusId, double balance)
  {
  // bind the parameters into the prepared statement
  int status = sqlite3_bind_text (m_setBalanceStmt, 
    sqlite3_bind_parameter_index (m_setBalanceStmt, "@cusId"),
    cusId.c_str (),
    cusId.size (),
    SQLITE_STATIC
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind cusId (" << cusId << ") into m_setBalanceStmt - ");

  status = sqlite3_bind_double (m_setBalanceStmt, 
    sqlite3_bind_parameter_index (m_setBalanceStmt, "@balance"),
    balance
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind balance (" << balance << ") into m_setBalanceStmt - ");

  // execute the SQL statement
  status = sqlite3_step (m_setBalanceStmt);
  SQLITE_STATUS_CHECK (status, "Unable to execute \"m_setBalanceStmt\" - ");
  status = sqlite3_reset (m_setBalanceStmt);
  SQLITE_STATUS_CHECK (status, "Unable to reset \"m_setBalanceStmt\" - ");
  }

double 
BankDatabase::GetBalance (std::string cusId)
  {
  // bind the parameters into the prepared statement
  int status = sqlite3_bind_text (m_getBalanceStmt, 
    sqlite3_bind_parameter_index (m_getBalanceStmt, "@cusId"),
    cusId.c_str (),
    cusId.size (),
    SQLITE_STATIC
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind cusId (" << cusId << ") into m_getBalanceStmt - ");

  // execute the SQL statement
  status = sqlite3_step (m_getBalanceStmt);
  SQLITE_STATUS_CHECK (status, "Unable to execute \"m_getBalanceStmt\" - ");

  // get the balance, if the customer does not exist yet, return 0
  double balance = 0;
  if (status == SQLITE_ROW)
    {
    balance = sqlite3_column_double (m_getBalanceStmt, 0);
    status = sqlite3_reset (m_getBalanceStmt);
    SQLITE_STATUS_CHECK (status, "Unable to reset \"m_getBalanceStmt\" - ");
    }
  else
    {
    status = sqlite3_reset (m_getBalanceStmt);
    SQLITE_STATUS_CHECK (status, "Unable to reset \"m_getBalanceStmt\" - ");

    string wrn;
    WRITE_STRING (wrn, "The customer ID (" << cusId << ") does not exist!");
    DEBUG_WRN (wrn);
    throw runtime_error (wrn);
    }

  return balance;
  }

void 
BankDatabase::BeginTransaction ()
  {
  // create the table if it does not exist yet
  char* errmsg;
  int status = sqlite3_exec (m_db,
    "begin transaction;",
    NULL, 
    NULL,
    &errmsg
    );
  SQLITE_STATUS_CHECK (status, errmsg << " - ");
  sqlite3_free (errmsg);
  }

bool 
BankDatabase::CommitTransaction ()
  {
  bool isDone = false;

  // create the table if it does not exist yet
  char* errmsg;
  int status = sqlite3_exec (m_db,
    "commit;",
    NULL, 
    NULL,
    &errmsg
    );
  SQLITE_STATUS_CHECK (status, errmsg << " - ");
  sqlite3_free (errmsg);

  return true;
  }

bool 
BankDatabase::RollbackTransaction ()
  {
  bool isDone = false;

  // create the table if it does not exist yet
  char* errmsg;
  int status = sqlite3_exec (m_db,
    "rollback;",
    NULL, 
    NULL,
    &errmsg
    );
  SQLITE_STATUS_CHECK (status, errmsg << " - ");
  sqlite3_free (errmsg);

  return true;
  }

int 
BankDatabase::GetUpdateCount (string clientId)
  {
  // bind the parameters into the prepared statement
  int status = sqlite3_bind_text (m_getUpdateCountStmt, 
    sqlite3_bind_parameter_index (m_getUpdateCountStmt, "@clientId"),
    clientId.c_str (),
    clientId.size (),
    SQLITE_STATIC
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind clientId (" << clientId << ") into m_getUpdateCountStmt - ");

  // execute the SQL statement
  status = sqlite3_step (m_getUpdateCountStmt);
  SQLITE_STATUS_CHECK (status, "Unable to execute \"m_getUpdateCountStmt\" - ");

  // get the balance, if the customer does not exist yet, return 0
  int updateCount = 0;
  if (status == SQLITE_ROW)
    {
    updateCount = sqlite3_column_int (m_getUpdateCountStmt, 0);
    }
  status = sqlite3_reset (m_getUpdateCountStmt);
  SQLITE_STATUS_CHECK (status, "Unable to reset \"m_getUpdateCountStmt\" - ");

  return updateCount;
  }

void 
BankDatabase::IncrementUpdateCount (string clientId)
  {
  int currentUpdateCount = GetUpdateCount (clientId);

  // bind the parameters into the prepared statement
  int status = sqlite3_bind_text (m_setUpdateCountStmt, 
    sqlite3_bind_parameter_index (m_setUpdateCountStmt, "@clientId"),
    clientId.c_str (),
    clientId.size (),
    SQLITE_STATIC
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind clientId (" << clientId << ") into m_setUpdateCountStmt - ");

  status = sqlite3_bind_double (m_setUpdateCountStmt, 
    sqlite3_bind_parameter_index (m_setUpdateCountStmt, "@updateCount"),
    currentUpdateCount + 1
    );
  SQLITE_STATUS_CHECK (status, "Unable to bind updateCount (" << currentUpdateCount + 1 << ") into m_setUpdateCountStmt - ");

  // execute the SQL statement
  status = sqlite3_step (m_setUpdateCountStmt);
  SQLITE_STATUS_CHECK (status, "Unable to execute \"m_setUpdateCountStmt\" - ");
  status = sqlite3_reset (m_setUpdateCountStmt);
  SQLITE_STATUS_CHECK (status, "Unable to reset \"m_setUpdateCountStmt\" - ");
  }

/////////////
//         //
// Testing //
//         //
/////////////

class BankDatabaseTest : public ::testing::Test
  {
  protected:
    virtual void SetUp()
      {
      bank.Open ("bank_testing.db");
      bank.TouchCustomer ("wilson");
      bank.TouchCustomer ("helen");
      }

    virtual void TearDown()
      {
      bank.Close ();
      system ("rm -f bank_testing.db");
      }

    BankDatabase bank;
  };

TEST_F (BankDatabaseTest, TouchCustomerTest)
  {
  bank.TouchCustomer ("abc");
  EXPECT_DOUBLE_EQ (0, bank.GetBalance ("abc"));

  EXPECT_THROW (bank.GetBalance ("cde"), runtime_error);

  bank.TouchCustomer ("def");
  EXPECT_DOUBLE_EQ (0, bank.GetBalance ("def"));

  bank.SetBalance ("abc", 10);
  bank.TouchCustomer ("abc");
  EXPECT_DOUBLE_EQ (10, bank.GetBalance ("abc"));
  }

TEST_F (BankDatabaseTest, GetSetBalanceTest)
  {
  // test if getting balance of a non-existing customer can return 0
  BankDatabase bank ("bank_testing.db");
  EXPECT_THROW (bank.GetBalance ("no-this-cus"), runtime_error);

  // test if getting balance of a valid customer can be done  
  bank.SetBalance ("wilson", 123.45);  
  EXPECT_DOUBLE_EQ (123.45, bank.GetBalance ("wilson"));

  // test if the balance can be changed
  bank.SetBalance ("wilson", 23.456);
  EXPECT_DOUBLE_EQ (23.456, bank.GetBalance ("wilson"));
  }

TEST_F (BankDatabaseTest, TransactionTest)
  {

  bank.SetBalance ("wilson", 123.45);
  bank.SetBalance ("helen", 0.0);

  bank.BeginTransaction ();
  bank.SetBalance ("helen", 789.45); 
  bank.SetBalance ("wilson", 456.12);
  EXPECT_TRUE (bank.RollbackTransaction ());

  EXPECT_DOUBLE_EQ (123.45, bank.GetBalance ("wilson")) << "Rollback failed!";
  EXPECT_DOUBLE_EQ (0.0, bank.GetBalance ("helen")) << "Rollback failed!";

  bank.BeginTransaction ();
  bank.SetBalance ("helen", 789.45);
  bank.SetBalance ("wilson", 456.12);
  EXPECT_TRUE (bank.CommitTransaction ());

  EXPECT_DOUBLE_EQ (789.45, bank.GetBalance ("helen")) << "commit failed!";
  EXPECT_DOUBLE_EQ (456.12, bank.GetBalance ("wilson")) << "commit failed!";
  }

TEST_F (BankDatabaseTest, UpdateCounterTest)
  {
  string clientId ("01");

  int updateCount = bank.GetUpdateCount (clientId);

  bank.IncrementUpdateCount (clientId);
  EXPECT_EQ (++updateCount, bank.GetUpdateCount (clientId));
  bank.IncrementUpdateCount (clientId);
  EXPECT_EQ (++updateCount, bank.GetUpdateCount (clientId));
  bank.IncrementUpdateCount (clientId);
  EXPECT_EQ (++updateCount, bank.GetUpdateCount (clientId));
  bank.IncrementUpdateCount (clientId);
  EXPECT_EQ (++updateCount, bank.GetUpdateCount (clientId));
  bank.IncrementUpdateCount (clientId);
  EXPECT_EQ (++updateCount, bank.GetUpdateCount (clientId));
  }
